package org.queueify.servlet;

import java.util.ConcurrentModificationException;


import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Transaction;

/*
 * This class reads a specific entity from the datastore within a transaction, and leases or surrenders a lock.
 * 
 * The isolation/consistency behavior is documented here:
 *   http://code.google.com/appengine/docs/java/datastore/transactions.html#Isolation_and_Consistency
 *   
 * Most important is this part from the docs, which states a 'get' inside a transaction should give me a consistent
 * view of the data:
 *   In a transaction, all reads reflect the current, consistent state of the datastore at the time the transaction
 *   started. This does not include previous puts and deletes inside the transaction. Queries and gets inside a
 *   transaction are guaranteed to see a single, consistent snapshot of the datastore as of the beginning of the transaction.
 *
 * 
 * I decided to make the lock a "lease", to ease the responsibility of having to surrender the lock when done.
 * 
 */

class QueueifySingletonMutex {

//	static private String QUEUEIFY_SINGLETON_MUTEX_ROOT = "QUEUEIFY_SINGLETON_MUTEX_ROOT";
	static private String QUEUEIFY_SINGLETON_MUTEX = "QUEUEIFY_SINGLETON_MUTEX";
	
//	static private Key rootKey = null;
//	static private Key getRootKey() {
//		if ( rootKey == null ) {
//			rootKey = KeyFactory.createKey(QUEUEIFY_SINGLETON_MUTEX_ROOT, 1);
//			DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
//			try {
//				Entity root = datastore.get(rootKey);
//				rootKey = root.getKey(); // this is not necessary...
//			}
//			catch (EntityNotFoundException e) {
//				Entity root = new Entity(rootKey);
//				rootKey = datastore.put(root);				
//			}
//		}
//		return rootKey;
//	}
	
	static public void leaseLock(String name, long leaseSeconds) throws QueueifyAlreadyLockedException {
		for (int i=0;i<50;i++) {
			try {
				leaseLockAttempt(name, leaseSeconds);
				return;
			}
			catch (ConcurrentModificationException e) {
				// stay in the loop and try again
			}
		}
		throw new ConcurrentModificationException("Could not modify lock for "+name+"/"+leaseSeconds+".");
	}

	static private void leaseLockAttempt(String name, long leaseSeconds) throws QueueifyAlreadyLockedException {
		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
		Transaction tx = datastore.beginTransaction();
		try {
			Entity mutex = getMutexEntity(datastore);
			Long lockExpire = (Long) mutex.getProperty(name); // zero means unlocked, positive value is the lock expiration time.
			if ( lockExpire == null ) {
				lockExpire = new Long(0);
			}
			if ( lockExpire > 0 ) {
				// it is locked. but is it expired?
				long now = System.currentTimeMillis();
				if ( now < lockExpire ) {
					throw new QueueifyAlreadyLockedException("Mutex is locked for "+name);
				}
				// else lock is expired, so fall through and create new lock. 
			}

			// create a new lock
			Long expireTimestamp = new Long(leaseSeconds*1000 + System.currentTimeMillis());
			mutex.setProperty(name, expireTimestamp);
			datastore.put(mutex);
			
			// commit our changes
			tx.commit();			
		}
		finally {
			if ( tx.isActive() ) {
				tx.rollback();
			}
		}
	}
	
	static public void surrenderLock(String name) {
		for (int i=0;i<50;i++) {
			try {
				surrenderLockAttempt(name);
				return;
			}
			catch (ConcurrentModificationException e) {
				// stay in the loop and try again
			}
		}
		throw new ConcurrentModificationException("Could not modify/surrender lock for "+name+".");
	}
	
	static private void surrenderLockAttempt(String name) {
		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
		Transaction tx = datastore.beginTransaction();
		try {
			Entity mutex = getMutexEntity(datastore);

			// eliminate lock
			Long expireTimestamp = new Long(0);
			mutex.setProperty(name, expireTimestamp);
			datastore.put(mutex);
			
			// commit our changes
			tx.commit();			
		}
		finally {
			if ( tx.isActive() ) {
				tx.rollback();
			}
		}
	}
	
	static private Entity getMutexEntity(DatastoreService datastore) {
		Key mutexKey = KeyFactory.createKey(QUEUEIFY_SINGLETON_MUTEX, 1);
		Entity mutex;
		try {
			mutex = datastore.get(mutexKey);
		}
		catch ( EntityNotFoundException e ) {
			mutex = new Entity(mutexKey);
			datastore.put(mutex);
		}
		return mutex;
	}

}
